Skip to main content

TRPerformance&Ordering

By default TableReplicator already does the right thing: writes are batched per frame and applied in a consistent order on every client. This guide explains those guarantees and the few knobs you can turn when a replicator has unusual needs.

Per-frame batching (the default)

When you mutate .Manager, the write isn't sent immediately. It's queued and flushed once per frame, so a burst of writes across many replicators collapses into at most one message per player per frame. On the client, a whole flush is applied inside a single TableManager batch, so listeners see one coalesced update rather than a storm of intermediate ones.

You rarely need to change this. The options below are for the cases where you do.

Coalesced — drop redundant writes

If a replicator writes the same key many times per frame and only the final value matters, set Coalesced = true. Intermediate writes to a key are dropped and only the latest is sent:

local replicator = ServerReplicator.new({
	Data = { MousePosition = Vector3.zero },
	Targets = "all",
	Coalesced = true, -- only the last MousePosition of the frame is sent
})

Best for high-churn, latest-value-wins data (cursor positions, live sliders). Avoid it when every intermediate value is meaningful.

ImmediateFlush — send each op right away

Set ImmediateFlush = true to send each op as it happens instead of waiting for the per-frame flush. The main use is guaranteeing a data change lands before an external RemoteEvent you fire in the same frame:

local replicator = ServerReplicator.new({
	Data = { State = "Idle" },
	Targets = "all",
	ImmediateFlush = true,
})
Mutually exclusive

Coalesced and ImmediateFlush cannot both be true — that combination throws at construction. Immediate flushing also gives up the batching win, so reach for it only when ordering against external events actually matters.

If you only need a one-off flush rather than a permanent mode, call the static ServerReplicator.FlushNow() to drain all pending ops immediately.

Ordering guarantees

Every replicator's data ops funnel through a single server-wide ordering point, so:

  • Global order is preserved. Ops from different replicators and different TableManagers replay on the client in the exact order the server produced them — not merely per-replicator order.
  • Audience is captured at write time, re-checked at flush. A player added mid-frame never double-applies an op already baked into their snapshot; a player removed mid-frame never receives an op for a replicator they were just dropped from.
  • Ordered remotes interleave with data. Signals and functions registered as ordered variants (see TR Custom Remotes) travel through the same buffer, so an ordered signal fired after a Set arrives after that Set is applied.

Discovery listener scheduling (FireMode)

Discovery callbacks (ForEach, OnNew, and the ReplicatorCreated signal) are scheduled according to a global fire mode. The default, "bindable", mirrors Roblox's BindableEvent behavior. You can change it for listeners registered afterward:

ServerReplicator.SetListenerFireMode("immediate") -- run on a fresh thread at once
ServerReplicator.SetListenerFireMode("deferred")  -- run at the end of the resumption cycle
ServerReplicator.SetListenerFireMode("bindable")  -- default, matches BindableEvent

print(ServerReplicator.ListenerFireMode) -- read-only current mode

"immediate" gives the lowest latency but runs callbacks re-entrantly; "deferred" is safest if a callback might create or destroy replicators. The setting is shared across ServerReplicator and ClientReplicator.

Going deeper

For the full internal picture — the frame buffer, the double task.defer, the wire format, and table de-duplication — see ARCHITECTURE.md in the TableReplicator source. This guide covers only the user-facing tuning knobs.


See also

Show raw api
{
    "functions": [],
    "properties": [],
    "types": [],
    "name": "TR Performance & Ordering",
    "desc": "By default TableReplicator already does the right thing: writes are batched per\nframe and applied in a consistent order on every client. This guide explains those\nguarantees and the few knobs you can turn when a replicator has unusual needs.\n\n### Per-frame batching (the default)\n\nWhen you mutate `.Manager`, the write isn't sent immediately. It's queued and flushed\nonce per frame, so a burst of writes across many replicators collapses into **at most\none message per player per frame**. On the client, a whole flush is applied inside a\nsingle `TableManager` batch, so listeners see one coalesced update rather than a\nstorm of intermediate ones.\n\nYou rarely need to change this. The options below are for the cases where you do.\n\n### `Coalesced` — drop redundant writes\n\nIf a replicator writes the same key many times per frame and only the final value\nmatters, set `Coalesced = true`. Intermediate writes to a key are dropped and only\nthe latest is sent:\n\n```lua\nlocal replicator = ServerReplicator.new({\n\tData = { MousePosition = Vector3.zero },\n\tTargets = \"all\",\n\tCoalesced = true, -- only the last MousePosition of the frame is sent\n})\n```\n\nBest for high-churn, latest-value-wins data (cursor positions, live sliders). Avoid\nit when every intermediate value is meaningful.\n\n### `ImmediateFlush` — send each op right away\n\nSet `ImmediateFlush = true` to send each op as it happens instead of waiting for the\nper-frame flush. The main use is guaranteeing a data change lands *before* an external\n`RemoteEvent` you fire in the same frame:\n\n```lua\nlocal replicator = ServerReplicator.new({\n\tData = { State = \"Idle\" },\n\tTargets = \"all\",\n\tImmediateFlush = true,\n})\n```\n\n:::caution Mutually exclusive\n`Coalesced` and `ImmediateFlush` cannot both be `true` — that combination throws at\nconstruction. Immediate flushing also gives up the batching win, so reach for it only\nwhen ordering against external events actually matters.\n:::\n\nIf you only need a one-off flush rather than a permanent mode, call the static\n`ServerReplicator.FlushNow()` to drain all pending ops immediately.\n\n### Ordering guarantees\n\nEvery replicator's data ops funnel through a single server-wide ordering point, so:\n\n- **Global order is preserved.** Ops from *different* replicators and different\n  `TableManager`s replay on the client in the exact order the server produced them —\n  not merely per-replicator order.\n- **Audience is captured at write time, re-checked at flush.** A player added\n  mid-frame never double-applies an op already baked into their snapshot; a player\n  removed mid-frame never receives an op for a replicator they were just dropped from.\n- **Ordered remotes interleave with data.** Signals and functions registered as\n  *ordered* variants (see [TR Custom Remotes](/api/TR%20Custom%20Remotes)) travel\n  through the same buffer, so an\n  ordered signal fired after a `Set` arrives after that `Set` is applied.\n\n### Discovery listener scheduling (`FireMode`)\n\nDiscovery callbacks (`ForEach`, `OnNew`, and the `ReplicatorCreated` signal) are\nscheduled according to a global fire mode. The default, `\"bindable\"`, mirrors\nRoblox's `BindableEvent` behavior. You can change it for listeners registered\nafterward:\n\n```lua\nServerReplicator.SetListenerFireMode(\"immediate\") -- run on a fresh thread at once\nServerReplicator.SetListenerFireMode(\"deferred\")  -- run at the end of the resumption cycle\nServerReplicator.SetListenerFireMode(\"bindable\")  -- default, matches BindableEvent\n\nprint(ServerReplicator.ListenerFireMode) -- read-only current mode\n```\n\n`\"immediate\"` gives the lowest latency but runs callbacks re-entrantly;\n`\"deferred\"` is safest if a callback might create or destroy replicators. The setting\nis shared across `ServerReplicator` and `ClientReplicator`.\n\n:::info Going deeper\nFor the full internal picture — the frame buffer, the double `task.defer`, the wire\nformat, and table de-duplication — see `ARCHITECTURE.md` in the TableReplicator\nsource. This guide covers only the user-facing tuning knobs.\n:::\n\n---\n### See also\n\n- **[TR Getting Started](/api/TR%20Getting%20Started)** — where `Coalesced` and `ImmediateFlush` live in the config.\n- **[TR Custom Remotes](/api/TR%20Custom%20Remotes)** — ordered vs unordered signals and functions.\n- **[TR Discovery & Targeting](/api/TR%20Discovery%20&%20Targeting)** — the listeners that `FireMode` schedules.",
    "source": {
        "line": 105,
        "path": "lib/tablereplicator/src/Docs/TR_Performance_And_Ordering.luau"
    }
}